/**********************************************************************

  Audacity: A Digital Audio Editor

  UndoManager.cpp

  Dominic Mazzoni

*******************************************************************//**

\class UndoManager
\brief Works with HistoryDialog to provide the Undo functionality.

*//****************************************************************//**

\class UndoStackElem
\brief Holds one item with description and time range for the
UndoManager

*//*******************************************************************/

#include "UndoManager.h"

#include <wx/hashset.h>

#include "BasicUI.h"
#include "Project.h"
#include "TransactionScope.h"
//#include "NoteTrack.h"  // for Sonify* function declarations

#include <unordered_map>

UndoStateExtension::~UndoStateExtension() = default;

bool UndoStateExtension::CanUndoOrRedo(const AudacityProject&)
{
    return true;
}

UndoRedoExtensionRegistry::Savers& UndoRedoExtensionRegistry::GetSavers()
{
    static Savers theSavers;
    return theSavers;
}

UndoState::Extensions UndoRedoExtensionRegistry::GetExtensions(AudacityProject& project)
{
    UndoState::Extensions result;
    for (auto& [typeIndex, saver] : GetSavers()) {
        if (saver) {
            result.emplace(typeIndex, saver(project));
        }
    }
    return result;
}

using SampleBlockID = long long;

static const AudacityProject::AttachedObjects::RegisteredFactory key{
    [](AudacityProject& project)
    { return std::make_unique<UndoManager>(project); }
};

UndoManager& UndoManager::Get(AudacityProject& project)
{
    return project.AttachedObjects::Get< UndoManager >(key);
}

const UndoManager& UndoManager::Get(const AudacityProject& project)
{
    return Get(const_cast< AudacityProject& >(project));
}

UndoManager::UndoManager(AudacityProject& project)
    : mProject{project}
{
    current = -1;
    saved = -1;
}

UndoManager::~UndoManager()
{
    wxASSERT(stack.empty());
}

void UndoManager::GetShortDescription(unsigned int n, TranslatableString* desc)
{
    wxASSERT(n < stack.size());

    *desc = stack[n]->shortDescription;
}

void UndoManager::SetLongDescription(
    unsigned int n, const TranslatableString& desc)
{
    n -= 1;

    wxASSERT(n < stack.size());

    stack[n]->description = desc;
}

void UndoManager::RemoveStateAt(int n)
{
    // Remove the state from the array first, and destroy it at function exit.
    // Because in case of callbacks from destruction of Sample blocks, there
    // might be a yield to GUI and other events might inspect the undo stack
    // (such as history window update).  Don't expose an inconsistent stack
    // state.
    auto iter = stack.begin() + n;
    auto state = std::move(*iter);
    stack.erase(iter);
}

void UndoManager::EnqueueMessage(UndoRedoMessage message)
{
    BasicUI::CallAfter([wThis = weak_from_this(), message]{
        if (auto pThis = wThis.lock()) {
            pThis->Publish(message);
        }
    });
}

void UndoManager::RemoveStates(size_t begin, size_t end)
{
    Publish({ UndoRedoMessage::BeginPurge, begin, end });
    auto cleanup
        =finally([&]{ Publish({ UndoRedoMessage::EndPurge }); });

    // Wrap the whole in a savepoint for better performance
    TransactionScope trans{ mProject, "DiscardingUndoStates" };

    for (size_t ii = begin; ii < end; ++ii) {
        if (current > begin) {
            --current;
        }
        if (saved > static_cast<int>(begin)) {
            --saved;
        }
        RemoveStateAt(begin);
    }

    // Success, commit the savepoint
    trans.Commit();

    if (begin != end) {
        EnqueueMessage({ UndoRedoMessage::Purge });
    }
}

void UndoManager::ClearStates()
{
    RemoveStates(0, stack.size());
    current = -1;
    saved = -1;
}

unsigned int UndoManager::GetNumStates()
{
    return stack.size();
}

unsigned int UndoManager::GetCurrentState()
{
    return current;
}

bool UndoManager::UndoAvailable()
{
    return CheckAvailable(current - 1);
}

bool UndoManager::RedoAvailable()
{
    return CheckAvailable(current + 1);
}

bool UndoManager::CheckAvailable(int index)
{
    if (index < 0 || index >= (int)stack.size()) {
        return false;
    }
    auto& extensions = stack[index]->state.extensions;
    return std::all_of(extensions.begin(), extensions.end(),
                       [&](std::pair<const std::type_index, std::shared_ptr<UndoStateExtension> >& entry){
        return !entry.second || entry.second->CanUndoOrRedo(mProject);
    });
}

void UndoManager::ModifyState()
{
    if (current == wxNOT_FOUND) {
        return;
    }

//   SonifyBeginModifyState();
    auto& state = stack[current]->state;

    // Re-create all captured project state
    state.extensions = UndoRedoExtensionRegistry::GetExtensions(mProject);

//   SonifyEndModifyState();

    EnqueueMessage({ UndoRedoMessage::Modified });
}

void UndoManager::ModifyState(const std::type_index& undoStateExtensionTypeIndex)
{
    if (current == wxNOT_FOUND) {
        return;
    }

    auto& extensions = stack[current]->state.extensions;

    std::unordered_map<std::type_index, UndoRedoExtensionRegistry::Saver>& savers = UndoRedoExtensionRegistry::GetSavers();
    const auto saverIt = savers.find(undoStateExtensionTypeIndex);
    const auto extensionIt = extensions.find(undoStateExtensionTypeIndex);
    if (extensionIt != extensions.end() && saverIt != savers.end() && saverIt->second) {
        auto updatedExtension = saverIt->second(mProject);
        extensions.insert_or_assign(undoStateExtensionTypeIndex, updatedExtension);
    }
}

void UndoManager::RenameState(int state,
                              const TranslatableString& longDescription,
                              const TranslatableString& shortDescription)
{
    if (state >= 0 && state < stack.size()) {
        auto& theState = *stack[state];
        theState.description = longDescription;
        theState.shortDescription = shortDescription;

        EnqueueMessage({ UndoRedoMessage::Renamed });
    }
}

void UndoManager::PushState(const TranslatableString& longDescription,
                            const TranslatableString& shortDescription,
                            UndoPush flags)
{
    if ((flags& UndoPush::CONSOLIDATE) != UndoPush::NONE
        &&// compare full translations not msgids!
        lastAction.Translation() == longDescription.Translation()
        && mayConsolidate) {
        ModifyState();
        // MB: If the "saved" state was modified by ModifyState, reset
        //  it so that UnsavedChanges returns true.
        if (current == saved) {
            saved = -1;
        }
        return;
    }

    mayConsolidate = true;

    AbandonRedo();

    stack.push_back(std::make_unique<UndoStackElem>(UndoRedoExtensionRegistry::GetExtensions(mProject), longDescription, shortDescription));

    current++;

    lastAction = longDescription;

    EnqueueMessage({ UndoRedoMessage::Pushed });
}

void UndoManager::AbandonRedo()
{
    if (saved > current) {
        saved = -1;
    }
    RemoveStates(current + 1, stack.size());
}

void UndoManager::SetStateTo(unsigned int n, const Consumer& consumer)
{
    wxASSERT(n < stack.size());

    current = n;

    lastAction = {};
    mayConsolidate = false;

    consumer(*stack[current]);

    EnqueueMessage({ UndoRedoMessage::Reset });
}

void UndoManager::Undo(const Consumer& consumer)
{
    wxASSERT(UndoAvailable());

    current--;

    lastAction = {};
    mayConsolidate = false;

    consumer(*stack[current]);

    EnqueueMessage({ UndoRedoMessage::UndoOrRedo });
}

void UndoManager::Redo(const Consumer& consumer)
{
    wxASSERT(RedoAvailable());

    current++;

    /*
    if (!RedoAvailable()) {
       *sel0 = stack[current]->sel0;
       *sel1 = stack[current]->sel1;
    }
    else {
       current++;
       *sel0 = stack[current]->sel0;
       *sel1 = stack[current]->sel1;
       current--;
    }
    */

    lastAction = {};
    mayConsolidate = false;

    consumer(*stack[current]);

    EnqueueMessage({ UndoRedoMessage::UndoOrRedo });
}

void UndoManager::VisitStates(const Consumer& consumer, bool newestFirst)
{
    auto fn = [&]( decltype(stack[0])& ptr ){ consumer(*ptr); };
    if (newestFirst) {
        std::for_each(stack.rbegin(), stack.rend(), fn);
    } else {
        std::for_each(stack.begin(), stack.end(), fn);
    }
}

void UndoManager::VisitStates(
    const Consumer& consumer, size_t begin, size_t end)
{
    auto size = stack.size();
    if (begin < end) {
        end = std::min(end, size);
        for (auto ii = begin; ii < end; ++ii) {
            consumer(*stack[ii]);
        }
    } else {
        if (size == 0) {
            return;
        }
        begin = std::min(begin, size - 1);
        for (auto ii = begin; ii > end; --ii) {
            consumer(*stack[ii]);
        }
    }
}

void UndoManager::MarkUnsaved()
{
    saved = -1;
}

bool UndoManager::UnsavedChanges() const
{
    return saved != current;
}

void UndoManager::StateSaved()
{
    saved = current;
}

int UndoManager::GetSavedState() const
{
    return saved;
}

// currently unused
//void UndoManager::Debug()
//{
//   for (unsigned int i = 0; i < stack.size(); i++) {
//      for (auto t : stack[i]->tracks->Any())
//         wxPrintf(wxT("*%d* %s %f\n"),
//                  i, (i == (unsigned int)current) ? wxT("-->") : wxT("   "),
//                t ? t->GetEndTime()-t->GetStartTime() : 0);
//   }
//}
